-- Copyright (c) 2011-2012 Casey Baxter
-- See LICENSE file for details

---------------------------------------------------------------------------------------------------
-- -= TileLayer =-
---------------------------------------------------------------------------------------------------
-- Setup
--local PATH = (...):gsub("[\\/]", ""):match(".+%.") or ''

local math = math
local type = type
local love = love
--local Grid = require(PATH .. "Grid")
local TileLayer = {}
TileLayer.class = "TileLayer"
TileLayer.__index = TileLayer
--TileLayer.__call = Grid.__call
----------------------------------------------------------------------------------------------------
-- GRID INTERFACE
function TileLayer:get(x,y)
    return self.cells[x] and self.cells[x][y]
end

TileLayer.__call = TileLayer.get

function TileLayer:set(x,y,value)
    if not self.cells[x] then self.cells[x] = {} end
    self.cells[x][y] = value
end

function TileLayer:clear()
    self.cells = {}
end

function TileLayer:rectangle(startX, startY, width, height, includeNil)
    local x, y = startX, startY
    return function()
        while y <= startY + height do
            while x <= startX + width do
                x = x+1
                if self(x-1,y) ~= nil or includeNil then 
                    return x-1, y, self(x-1,y)
                end
            end
            x = startX
            y = y+1
        end
        return nil
    end
end



----------------------------------------------------------------------------------------------------
-- Returns a new TileLayer
function TileLayer.init(o)
	local tl = setmetatable(o, TileLayer)
	tl.cells = {}

    tl.parallaxX = tl.properties.parallaxx or 1      -- The horizontal speed of the parallax. 1 is normal
    tl.parallaxY = tl.properties.parallaxy or 1      -- The vertical speed of the parallax. 1 is normal
    --tl.offsetX = 0          -- Drawing offset X
    --tl.offsetY = 0          -- Drawing offset Y
    
    -- Private:
    tl._redraw = true                   -- If true then the layer needs to redraw tis sprite batches.
    tl._tileRange = {0,0,0,0}           -- Keeps the drawn tile range for the layer
    tl._previousTileRange = {0,0,0,0}   -- Previous _tileRange
    tl._batches = {}                    -- Keeps track of the sprite batches for each tileset
    tl._flippedTiles = {}               -- Stores the flipped tile locations. 
                                            -- 1 = flipped X, 2 = flipped Y, 3 = both
    tl._afterTileFunction = nil         -- A function that handles drawing things after individual
                                            -- tiles. This will not work with SpriteBatches.
    tl._afterTileArgs = nil             -- Starting arguments for the after tile function
    tl._previousUseSpriteBatch = false  -- The previous useSpriteBatch. If this is different then we 
                                            -- need to force a special redraw
    return tl
end

----------------------------------------------------------------------------------------------------
-- Clears the tile layer of its after tile function.
function TileLayer:clearAfterTileFunction()
    self._afterTileFunction = nil
    self._afterTileArgs = nil
end

----------------------------------------------------------------------------------------------------
-- Sets a function that will be called after every tile. funct(layer, x, y, drawX, drawY ...)
function TileLayer:setAfterTileFunction(funct, ...)
    self._afterTileFunction = funct
    local args = {...}
    if #args > 0 then self._afterTileArgs = args end
end

----------------------------------------------------------------------------------------------------
-- These are used in TileLayer:draw() but since that function is called so often we'll define them
-- outside to prevent them from being created and destroyed all the time.
local map, tile, tiles, postDraw, useSpriteBatch, tile, width, height
local at, drawX, drawY, flipX, flipY, r, g, b, a, halfW, halfH, rot
local x1, y1, x2, y2
-- Draws the TileLayer.
function TileLayer:draw() --FIXME: holycow!

    -- Early exit of the layer is not visible
    if not self.visible then return end

    -- We access these a lot so we'll shorted them a bit. 
    map, tiles = self.map, self.map.tiles
    postDraw = self.postDraw
    
    -- If useSpriteBatch was changed then we need to force the sprite batches to redraw.
    if self.useSpriteBatch ~= self._previousUseSpriteBatch then map:forceRedraw() end
    
    -- Set the previous useSpriteBatch
    self._previousUseSpriteBatch = self.useSpriteBatch
    
    -- If useSpriteBatch is set for this layer then use that, otherwise use the map's setting.
    useSpriteBatch = self.useSpriteBatch ~= nil and self.useSpriteBatch or map.useSpriteBatch
    
    -- We'll blend the set alpha in with the current alpha
    r,g,b,a = love.graphics.getColor()
    love.graphics.setColor(r,g,b, a*self.opacity)
    
    -- Clear sprite batches if the screen has changed.
    if self._redraw and useSpriteBatch then
        for k,v in pairs(self._batches) do
            v:clear()
        end
    end
    
    -- Get the tile range
    x1, y1, x2, y2 = self._tileRange[1], self._tileRange[2], self._tileRange[3], self._tileRange[4]
    
    -- Translate for the parallax
    if self.parallaxX ~= 1 or self.parallaxY ~= 1 then
        love.graphics.push()
        love.graphics.translate(math.floor(map.viewX - map.viewX*self.parallaxX), 
                                math.floor(map.viewY - map.viewY*self.parallaxY))
    end
    
    -- Only draw if we're not using sprite batches or we need to update the sprite batches.
    if self._redraw or not useSpriteBatch then
    
        -- Bind the sprite batches
        if useSpriteBatch then 
            --for k, batch in pairs(self._batches) do
            --    batch:bind()
            --end
        end
    
        -- Orthogonal tiles
        if map.orientation == "orthogonal" then
        
            -- Go through each tile
            for x,y,tile in self:rectangle(x1,y1,x2,y2) do
            
                -- Get the half-width and half-height
                halfW, halfH = tile.width*0.5, tile.height*0.5
                
                -- Draw the tile from the bottom left corner
                drawX, drawY = (x)*map.tileWidth, (y+1)*map.tileHeight
                
                -- Apply the offset
                drawX = drawX - map.offsetX - self.offsetX
                drawY = drawY - map.offsetY - self.offsetY
                
                -- Get the flipped tiles
				local ft = self._flippedTiles[x]
				ft = ft and ft[y]
                if ft then
                    rot =  (ft % 2) == 1 and true or false
                    flipY = (ft % 4) >= 2 and -1 or 1
                    flipX = ft >= 4 and -1 or 1
                    if rot then flipX, flipY = -flipY, flipX end
                else
                    rot, flipX, flipY = false, 1, 1
                end
                
                -- If we are using spritebatches
                if useSpriteBatch then
                    -- If we dont have a spritebatch for the current tile's tileset then make one
                    if not self._batches[tile.tileset] then 
                        self._batches[tile.tileset] = love.graphics.newSpriteBatch(
                                                        tile.tileset.image, map.width * map.height)
                        --self._batches[tile.tileset]:bind()
                    end
                    -- Add the quad to the spritebatch
                    self._batches[tile.tileset]:add(tile.quad, drawX + halfW, 
                                    drawY - halfH, 
                                    rot and math.pi*1.5 or 0, 
                                    flipX, flipY, halfW, halfH)
                                    
                -- If we are not using spritebatches
                else
                    -- Draw the tile
                    tile:draw(drawX + halfW,
                          drawY - halfH, 
                          rot and math.pi*1.5 or 0, 
                          flipX, flipY, halfW, halfH)
                          
                    -- Call the after tile function
                    if self._afterTileFunction then 
                        if self._afterTileArgs then
                            self._afterTileFunction(self, x, y, drawX, drawY, 
                                                    unpack(self._afterTileArgs))
                        else
                            self._afterTileFunction(self, x, y, drawX, drawY)
                        end
                    end
                end
            end
        end
        
        -- Isometric tiles
        if map.orientation == "isometric" then
            local x,y
            
            -- Get the starting x drawing location
            local draw_start = map.height * map.tileWidth/2
            
            -- Draw each tile starting from the top left tile. Make sure we have enough
            -- room to draw the widest and tallest tile in the map.
            for down=0,y2 do 
                for layer=0,1 do
                    for right=0,x2 do
                        x = x1 + right + down + layer - 1
                        y = y1 - right + down - 1
                        
                        -- If there is a tile row
						tile = self(x,y)
                        if tile then
                        
                            -- Check and see if the tile is flipped
							local ft = self.__flippedTiles[x]
							ft = ft and ft[y]
                            if ft then
                                rot =  (ft % 2) == 1 and true or false
                                flipY = (ft % 4) >= 2 and -1 or 1
                                flipX = ft >= 4 and -1 or 1
                                if rot then flipX, flipY = -flipY, flipX end
                            else
                                rot, flipX, flipY = false, 1, 1
                            end
                            
                            -- Get the tile
                            --tile = self(x,y)
                            
                            -- If the tile exists then draw the tile
                            --if tile then 
                            
                            -- Get the half-width and half-height
                            halfW, halfH = tile.width*0.5, tile.height*0.5
                            
                            -- Get the tile draw location
                            drawX = math.floor(draw_start + map.tileWidth/2 * (x - y-2))
                            drawY = math.floor(map.tileHeight/2 * (x + y+2))
                            
                            -- Apply the offset
                            drawX = drawX - map.offsetX - self.offsetX
                            drawY = drawY - map.offsetY - self.offsetY
                            
                            -- Using sprite batches
                            if useSpriteBatch then
                                -- If we dont have a spritebatch for the current tile's tileset 
                                -- then make one
                                if not self._batches[tile.tileset] then 
                                    self._batches[tile.tileset] = love.graphics.newSpriteBatch(
                                                                        tile.tileset.image, 
                                                                        map.width * map.height)
                                    -- Bind the sprite batch
                                    self._batches[tile.tileset]:bind()
                                end
                                -- Add the tile to the sprite batch.
                                self._batches[tile.tileset]:add(tile.quad, drawX + halfW + 
                                                            (rot and halfW or 0), 
                                                            drawY-halfH+(rot and halfW or 0), 
                                                            rot and math.pi*1.5 or 0, 
                                                            flipX, flipY, halfW, halfH)
                                                                
                            -- Not using sprite batches
                            else
                                tile:draw(drawX + halfW + (rot and halfW or 0), 
                                            drawY - halfH + (rot and halfW or 0), 
                                            rot and math.pi*1.5 or 0, 
                                            flipX, flipY, halfW, halfH)
                                            
                                -- Call the after tile function
                                if self._afterTileFunction then 
                                    if self._afterTileArgs then
                                        self._afterTileFunction(self, x, y, drawX, drawY,
                                                                unpack(self._afterTileArgs))
                                    else
                                        self._afterTileFunction(self, x, y, drawX, drawY)
                                    end
                                end
                                
                            end
                            --end
                        end
                    end
                end
            end
        end
        
        -- Unbind the sprite batches
         if useSpriteBatch then 
            --for k, batch in pairs(self._batches) do
            --    batch:unbind()
            --end
        end
        
    end
    
    -- We finished redrawing
    self._redraw = false
    
    -- If sprite batches are turned on then render them
    if useSpriteBatch then
        for k, batch in pairs(self._batches) do
            love.graphics.draw(batch)
        end
    end
    
    -- If we applied a translation for our parallax then remove it
    if self.parallaxX ~= 1 or self.parallaxY ~= 1 then
        love.graphics.pop()
    end
    
    -- Change the color back
    love.graphics.setColor(r,g,b,a)
end

----------------------------------------------------------------------------------------------------
-- This copies a tile so that you can paste it in another spot. The pasted tile will keep the
-- rotation and flipped status. You can copy and paste between layers.
local flippedVal = 2^29
function TileLayer:tileCopy(x,y)
    if not self(x,y) then 
        self.map._tileClipboard = 0 
    else
		local ft = self._flippedTiles[x]
        self.map._tileClipboard = self(x,y).id + ((ft and ft[y]) or 0) * flippedVal
    end
end

----------------------------------------------------------------------------------------------------
-- Paste a copied tile.
function TileLayer:tilePaste(x,y)
    self._redraw = true
    if not self.map._tileClipboard then
        error("TileLayer:tilePaste() - A tile must be copied with tileCopy() before pasting")
    end
    local clip = self.map._tileClipboard 
    if clip / flippedVal > 0 then
		local fts = self._flippedTiles
		if not fts[x] then fts[x] = {} end
		fts[x][y] = math.floor(clip / flippedVal)
    end
    self:set(x, y, self.map.tiles[clip % flippedVal])
end

----------------------------------------------------------------------------------------------------
-- Flip the tile's X. If doFlip is not specified then the flip is toggled.
function TileLayer:tileFlipX(x, y, doFlip)
	local fts = self._flippedTiles
	if not fts[x] then fts[x] = {} end
    self._redraw = true
    local flip = fts[x][y] or 0
    if doFlip ~= false and flip < 4 then 
        flip = flip + 4
    elseif doFlip ~= true and flip >= 4 then 
        flip = flip - 4
    end
	fts[x][y] = flip ~= 0 and flip or nil
end

----------------------------------------------------------------------------------------------------
-- Flip the tile's Y. If doFlip is not specified then the flip is toggled.
function TileLayer:tileFlipY(x, y, doFlip)
	local fts = self._flippedTiles
	if not fts[x] then fts[x] = {} end
    self.redraw = true
    local flip = fts[x][y] or 0
    if doFlip ~= false and flip % 4 < 2 then 
        flip = flip + 2
    elseif doFlip ~= true and flip % 4 >= 2 then 
        flip = flip - 2
    end
    fts[x][y] = flip ~= 0 and flip or nil
end

----------------------------------------------------------------------------------------------------
-- Rotate the tile.
function TileLayer:tileRotate(x, y, rot)
	local fts = self._flippedTiles
	if not fts[x] then fts[x] = {} end
	
    local flip = fts[x][y] or 0
    if rot then flip = rot % 8
    elseif flip == 0 then flip = 5
    elseif flip == 1 then flip = 4 
    elseif flip == 2 then flip = 1
    elseif flip == 3 then flip = 0 
    elseif flip == 4 then flip = 7
    elseif flip == 5 then flip = 6
    elseif flip == 6 then flip = 3
    elseif flip == 7 then flip = 2 
    end
	
	fts[x][y] = flip ~= 0 and flip or 0
end

----------------------------------------------------------------------------------------------------
-- Private
----------------------------------------------------------------------------------------------------

local FLIPPED = 0x20000000
function TileLayer:_populate(t)
	self:clear()
	
	local map = self.map
	local width, height =  map.width, map.height
	local tiles = map.tiles
	local tid
	for x = 0, width - 1 do
		for y = 0, height - 1 do
			tid = t[width * y + x + 1]
			if tid then
				if tid >= FLIPPED then
					local fts = self._flippedTiles
					if not fts[x] then fts[x] = {} end
					fts[x][y] = math.floor(tid / FLIPPED)
					tid = tid % FLIPPED
				end
				self:set(x, y, tiles[tid])
			end
		end
	end
end

return TileLayer
